import {
  BufferGeometryLoader,
  FileLoader,
  Loader,
  Object3D,
  MeshStandardMaterial,
  Mesh,
  Color,
  Points,
  PointsMaterial,
  Line,
  LineBasicMaterial,
  Matrix4,
  DirectionalLight,
  PointLight,
  SpotLight,
  RectAreaLight,
  Vector3,
  Sprite,
  SpriteMaterial,
  CanvasTexture,
  LinearFilter,
  ClampToEdgeWrapping,
  TextureLoader,
} from 'three'

const _taskCache = new WeakMap()

class Rhino3dmLoader extends Loader {
  constructor(manager) {
    super(manager)

    this.libraryPath = ''
    this.libraryPending = null
    this.libraryBinary = null
    this.libraryConfig = {}

    this.url = ''

    this.workerLimit = 4
    this.workerPool = []
    this.workerNextTaskID = 1
    this.workerSourceURL = ''
    this.workerConfig = {}

    this.materials = []
  }

  setLibraryPath(path) {
    this.libraryPath = path

    return this
  }

  setWorkerLimit(workerLimit) {
    this.workerLimit = workerLimit

    return this
  }

  load(url, onLoad, onProgress, onError) {
    const loader = new FileLoader(this.manager)

    loader.setPath(this.path)
    loader.setResponseType('arraybuffer')
    loader.setRequestHeader(this.requestHeader)

    this.url = url

    loader.load(
      url,
      (buffer) => {
        // Check for an existing task using this buffer. A transferred buffer cannot be transferred
        // again from this thread.
        if (_taskCache.has(buffer)) {
          const cachedTask = _taskCache.get(buffer)

          return cachedTask.promise.then(onLoad).catch(onError)
        }

        this.decodeObjects(buffer, url).then(onLoad).catch(onError)
      },
      onProgress,
      onError,
    )
  }

  debug() {
    console.log(
      'Task load: ',
      this.workerPool.map((worker) => worker._taskLoad),
    )
  }

  decodeObjects(buffer, url) {
    let worker
    let taskID

    const taskCost = buffer.byteLength

    const objectPending = this._getWorker(taskCost)
      .then((_worker) => {
        worker = _worker
        taskID = this.workerNextTaskID++ //hmmm

        return new Promise((resolve, reject) => {
          worker._callbacks[taskID] = { resolve, reject }

          worker.postMessage({ type: 'decode', id: taskID, buffer }, [buffer])

          //this.debug();
        })
      })
      .then((message) => this._createGeometry(message.data))

    // Remove task from the task list.
    // Note: replaced '.finally()' with '.catch().then()' block - iOS 11 support (#19416)
    objectPending
      .catch(() => true)
      .then(() => {
        if (worker && taskID) {
          this._releaseTask(worker, taskID)

          //this.debug();
        }
      })

    // Cache the task result.
    _taskCache.set(buffer, {
      url: url,
      promise: objectPending,
    })

    return objectPending
  }

  parse(data, onLoad, onError) {
    this.decodeObjects(data, '').then(onLoad).catch(onError)
  }

  _compareMaterials(material) {
    const mat = {}
    mat.name = material.name
    mat.color = {}
    mat.color.r = material.color.r
    mat.color.g = material.color.g
    mat.color.b = material.color.b
    mat.type = material.type

    for (let i = 0; i < this.materials.length; i++) {
      const m = this.materials[i]
      const _mat = {}
      _mat.name = m.name
      _mat.color = {}
      _mat.color.r = m.color.r
      _mat.color.g = m.color.g
      _mat.color.b = m.color.b
      _mat.type = m.type

      if (JSON.stringify(mat) === JSON.stringify(_mat)) {
        return m
      }
    }

    this.materials.push(material)

    return material
  }

  _createMaterial(material) {
    if (material === undefined) {
      return new MeshStandardMaterial({
        color: new Color(1, 1, 1),
        metalness: 0.8,
        name: 'default',
        side: 2,
      })
    }

    const _diffuseColor = material.diffuseColor

    const diffusecolor = new Color(_diffuseColor.r / 255.0, _diffuseColor.g / 255.0, _diffuseColor.b / 255.0)

    if (_diffuseColor.r === 0 && _diffuseColor.g === 0 && _diffuseColor.b === 0) {
      diffusecolor.r = 1
      diffusecolor.g = 1
      diffusecolor.b = 1
    }

    // console.log( material );

    const mat = new MeshStandardMaterial({
      color: diffusecolor,
      name: material.name,
      side: 2,
      transparent: material.transparency > 0 ? true : false,
      opacity: 1.0 - material.transparency,
    })

    const textureLoader = new TextureLoader()

    for (let i = 0; i < material.textures.length; i++) {
      const texture = material.textures[i]

      if (texture.image !== null) {
        const map = textureLoader.load(texture.image)

        switch (texture.type) {
          case 'Diffuse':
            mat.map = map

            break

          case 'Bump':
            mat.bumpMap = map

            break

          case 'Transparency':
            mat.alphaMap = map
            mat.transparent = true

            break

          case 'Emap':
            mat.envMap = map

            break
        }
      }
    }

    return mat
  }

  _createGeometry(data) {
    // console.log(data);

    const object = new Object3D()
    const instanceDefinitionObjects = []
    const instanceDefinitions = []
    const instanceReferences = []

    object.userData['layers'] = data.layers
    object.userData['groups'] = data.groups
    object.userData['settings'] = data.settings
    object.userData['objectType'] = 'File3dm'
    object.userData['materials'] = null
    object.name = this.url

    let objects = data.objects
    const materials = data.materials

    for (let i = 0; i < objects.length; i++) {
      const obj = objects[i]
      const attributes = obj.attributes

      switch (obj.objectType) {
        case 'InstanceDefinition':
          instanceDefinitions.push(obj)

          break

        case 'InstanceReference':
          instanceReferences.push(obj)

          break

        default:
          let _object

          if (attributes.materialIndex >= 0) {
            const rMaterial = materials[attributes.materialIndex]
            let material = this._createMaterial(rMaterial)
            material = this._compareMaterials(material)
            _object = this._createObject(obj, material)
          } else {
            const material = this._createMaterial()
            _object = this._createObject(obj, material)
          }

          if (_object === undefined) {
            continue
          }

          const layer = data.layers[attributes.layerIndex]

          _object.visible = layer ? data.layers[attributes.layerIndex].visible : true

          if (attributes.isInstanceDefinitionObject) {
            instanceDefinitionObjects.push(_object)
          } else {
            object.add(_object)
          }

          break
      }
    }

    for (let i = 0; i < instanceDefinitions.length; i++) {
      const iDef = instanceDefinitions[i]

      objects = []

      for (let j = 0; j < iDef.attributes.objectIds.length; j++) {
        const objId = iDef.attributes.objectIds[j]

        for (let p = 0; p < instanceDefinitionObjects.length; p++) {
          const idoId = instanceDefinitionObjects[p].userData.attributes.id

          if (objId === idoId) {
            objects.push(instanceDefinitionObjects[p])
          }
        }
      }

      // Currently clones geometry and does not take advantage of instancing

      for (let j = 0; j < instanceReferences.length; j++) {
        const iRef = instanceReferences[j]

        if (iRef.geometry.parentIdefId === iDef.attributes.id) {
          const iRefObject = new Object3D()
          const xf = iRef.geometry.xform.array

          const matrix = new Matrix4()
          matrix.set(
            xf[0],
            xf[1],
            xf[2],
            xf[3],
            xf[4],
            xf[5],
            xf[6],
            xf[7],
            xf[8],
            xf[9],
            xf[10],
            xf[11],
            xf[12],
            xf[13],
            xf[14],
            xf[15],
          )

          iRefObject.applyMatrix4(matrix)

          for (let p = 0; p < objects.length; p++) {
            iRefObject.add(objects[p].clone(true))
          }

          object.add(iRefObject)
        }
      }
    }

    object.userData['materials'] = this.materials
    return object
  }

  _createObject(obj, mat) {
    const loader = new BufferGeometryLoader()

    const attributes = obj.attributes

    let geometry, material, _color, color

    switch (obj.objectType) {
      case 'Point':
      case 'PointSet':
        geometry = loader.parse(obj.geometry)

        if (geometry.attributes.hasOwnProperty('color')) {
          material = new PointsMaterial({ vertexColors: true, sizeAttenuation: false, size: 2 })
        } else {
          _color = attributes.drawColor
          color = new Color(_color.r / 255.0, _color.g / 255.0, _color.b / 255.0)
          material = new PointsMaterial({ color: color, sizeAttenuation: false, size: 2 })
        }

        material = this._compareMaterials(material)

        const points = new Points(geometry, material)
        points.userData['attributes'] = attributes
        points.userData['objectType'] = obj.objectType

        if (attributes.name) {
          points.name = attributes.name
        }

        return points

      case 'Mesh':
      case 'Extrusion':
      case 'SubD':
      case 'Brep':
        if (obj.geometry === null) return

        geometry = loader.parse(obj.geometry)

        if (geometry.attributes.hasOwnProperty('color')) {
          mat.vertexColors = true
        }

        if (mat === null) {
          mat = this._createMaterial()
          mat = this._compareMaterials(mat)
        }

        const mesh = new Mesh(geometry, mat)
        mesh.castShadow = attributes.castsShadows
        mesh.receiveShadow = attributes.receivesShadows
        mesh.userData['attributes'] = attributes
        mesh.userData['objectType'] = obj.objectType

        if (attributes.name) {
          mesh.name = attributes.name
        }

        return mesh

      case 'Curve':
        geometry = loader.parse(obj.geometry)

        _color = attributes.drawColor
        color = new Color(_color.r / 255.0, _color.g / 255.0, _color.b / 255.0)

        material = new LineBasicMaterial({ color: color })
        material = this._compareMaterials(material)

        const lines = new Line(geometry, material)
        lines.userData['attributes'] = attributes
        lines.userData['objectType'] = obj.objectType

        if (attributes.name) {
          lines.name = attributes.name
        }

        return lines

      case 'TextDot':
        geometry = obj.geometry

        const ctx = document.createElement('canvas').getContext('2d')
        const font = `${geometry.fontHeight}px ${geometry.fontFace}`
        ctx.font = font
        const width = ctx.measureText(geometry.text).width + 10
        const height = geometry.fontHeight + 10

        const r = window.devicePixelRatio

        ctx.canvas.width = width * r
        ctx.canvas.height = height * r
        ctx.canvas.style.width = width + 'px'
        ctx.canvas.style.height = height + 'px'
        ctx.setTransform(r, 0, 0, r, 0, 0)

        ctx.font = font
        ctx.textBaseline = 'middle'
        ctx.textAlign = 'center'
        color = attributes.drawColor
        ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${color.a})`
        ctx.fillRect(0, 0, width, height)
        ctx.fillStyle = 'white'
        ctx.fillText(geometry.text, width / 2, height / 2)

        const texture = new CanvasTexture(ctx.canvas)
        texture.minFilter = LinearFilter
        texture.wrapS = ClampToEdgeWrapping
        texture.wrapT = ClampToEdgeWrapping

        material = new SpriteMaterial({ map: texture, depthTest: false })
        const sprite = new Sprite(material)
        sprite.position.set(geometry.point[0], geometry.point[1], geometry.point[2])
        sprite.scale.set(width / 10, height / 10, 1.0)

        sprite.userData['attributes'] = attributes
        sprite.userData['objectType'] = obj.objectType

        if (attributes.name) {
          sprite.name = attributes.name
        }

        return sprite

      case 'Light':
        geometry = obj.geometry

        let light

        if (geometry.isDirectionalLight) {
          light = new DirectionalLight()
          light.castShadow = attributes.castsShadows
          light.position.set(geometry.location[0], geometry.location[1], geometry.location[2])
          light.target.position.set(geometry.direction[0], geometry.direction[1], geometry.direction[2])
          light.shadow.normalBias = 0.1
        } else if (geometry.isPointLight) {
          light = new PointLight()
          light.castShadow = attributes.castsShadows
          light.position.set(geometry.location[0], geometry.location[1], geometry.location[2])
          light.shadow.normalBias = 0.1
        } else if (geometry.isRectangularLight) {
          light = new RectAreaLight()

          const width = Math.abs(geometry.width[2])
          const height = Math.abs(geometry.length[0])

          light.position.set(geometry.location[0] - height / 2, geometry.location[1], geometry.location[2] - width / 2)

          light.height = height
          light.width = width

          light.lookAt(new Vector3(geometry.direction[0], geometry.direction[1], geometry.direction[2]))
        } else if (geometry.isSpotLight) {
          light = new SpotLight()
          light.castShadow = attributes.castsShadows
          light.position.set(geometry.location[0], geometry.location[1], geometry.location[2])
          light.target.position.set(geometry.direction[0], geometry.direction[1], geometry.direction[2])
          light.angle = geometry.spotAngleRadians
          light.shadow.normalBias = 0.1
        } else if (geometry.isLinearLight) {
          console.warn('THREE.3DMLoader:  No conversion exists for linear lights.')

          return
        }

        if (light) {
          light.intensity = geometry.intensity
          _color = geometry.diffuse
          color = new Color(_color.r / 255.0, _color.g / 255.0, _color.b / 255.0)
          light.color = color
          light.userData['attributes'] = attributes
          light.userData['objectType'] = obj.objectType
        }

        return light
    }
  }

  _initLibrary() {
    if (!this.libraryPending) {
      // Load rhino3dm wrapper.
      const jsLoader = new FileLoader(this.manager)
      jsLoader.setPath(this.libraryPath)
      const jsContent = new Promise((resolve, reject) => {
        jsLoader.load('rhino3dm.js', resolve, undefined, reject)
      })

      // Load rhino3dm WASM binary.
      const binaryLoader = new FileLoader(this.manager)
      binaryLoader.setPath(this.libraryPath)
      binaryLoader.setResponseType('arraybuffer')
      const binaryContent = new Promise((resolve, reject) => {
        binaryLoader.load('rhino3dm.wasm', resolve, undefined, reject)
      })

      this.libraryPending = Promise.all([jsContent, binaryContent]).then(([jsContent, binaryContent]) => {
        //this.libraryBinary = binaryContent;
        this.libraryConfig.wasmBinary = binaryContent

        const fn = Rhino3dmWorker.toString()

        const body = [
          '/* rhino3dm.js */',
          jsContent,
          '/* worker */',
          fn.substring(fn.indexOf('{') + 1, fn.lastIndexOf('}')),
        ].join('\n')

        this.workerSourceURL = URL.createObjectURL(new Blob([body]))
      })
    }

    return this.libraryPending
  }

  _getWorker(taskCost) {
    return this._initLibrary().then(() => {
      if (this.workerPool.length < this.workerLimit) {
        const worker = new Worker(this.workerSourceURL)

        worker._callbacks = {}
        worker._taskCosts = {}
        worker._taskLoad = 0

        worker.postMessage({
          type: 'init',
          libraryConfig: this.libraryConfig,
        })

        worker.onmessage = function (e) {
          const message = e.data

          switch (message.type) {
            case 'decode':
              worker._callbacks[message.id].resolve(message)
              break

            case 'error':
              worker._callbacks[message.id].reject(message)
              break

            default:
              console.error('THREE.Rhino3dmLoader: Unexpected message, "' + message.type + '"')
          }
        }

        this.workerPool.push(worker)
      } else {
        this.workerPool.sort(function (a, b) {
          return a._taskLoad > b._taskLoad ? -1 : 1
        })
      }

      const worker = this.workerPool[this.workerPool.length - 1]

      worker._taskLoad += taskCost

      return worker
    })
  }

  _releaseTask(worker, taskID) {
    worker._taskLoad -= worker._taskCosts[taskID]
    delete worker._callbacks[taskID]
    delete worker._taskCosts[taskID]
  }

  dispose() {
    for (let i = 0; i < this.workerPool.length; ++i) {
      this.workerPool[i].terminate()
    }

    this.workerPool.length = 0

    return this
  }
}

/* WEB WORKER */

function Rhino3dmWorker() {
  let libraryPending
  let libraryConfig
  let rhino

  onmessage = function (e) {
    const message = e.data

    switch (message.type) {
      case 'init':
        libraryConfig = message.libraryConfig
        const wasmBinary = libraryConfig.wasmBinary
        let RhinoModule
        libraryPending = new Promise(function (resolve) {
          /* Like Basis Loader */
          RhinoModule = { wasmBinary, onRuntimeInitialized: resolve }

          rhino3dm(RhinoModule)
        }).then(() => {
          rhino = RhinoModule
        })

        break

      case 'decode':
        const buffer = message.buffer
        libraryPending.then(() => {
          const data = decodeObjects(rhino, buffer)

          self.postMessage({ type: 'decode', id: message.id, data })
        })

        break
    }
  }

  function decodeObjects(rhino, buffer) {
    const arr = new Uint8Array(buffer)
    const doc = rhino.File3dm.fromByteArray(arr)

    const objects = []
    const materials = []
    const layers = []
    const views = []
    const namedViews = []
    const groups = []

    //Handle objects

    const objs = doc.objects()
    const cnt = objs.count

    for (let i = 0; i < cnt; i++) {
      const _object = objs.get(i)

      const object = extractObjectData(_object, doc)

      _object.delete()

      if (object) {
        objects.push(object)
      }
    }

    // Handle instance definitions
    // console.log( `Instance Definitions Count: ${doc.instanceDefinitions().count()}` );

    for (let i = 0; i < doc.instanceDefinitions().count(); i++) {
      const idef = doc.instanceDefinitions().get(i)
      const idefAttributes = extractProperties(idef)
      idefAttributes.objectIds = idef.getObjectIds()

      objects.push({ geometry: null, attributes: idefAttributes, objectType: 'InstanceDefinition' })
    }

    // Handle materials

    const textureTypes = [
      // rhino.TextureType.Bitmap,
      rhino.TextureType.Diffuse,
      rhino.TextureType.Bump,
      rhino.TextureType.Transparency,
      rhino.TextureType.Opacity,
      rhino.TextureType.Emap,
    ]

    const pbrTextureTypes = [
      rhino.TextureType.PBR_BaseColor,
      rhino.TextureType.PBR_Subsurface,
      rhino.TextureType.PBR_SubsurfaceScattering,
      rhino.TextureType.PBR_SubsurfaceScatteringRadius,
      rhino.TextureType.PBR_Metallic,
      rhino.TextureType.PBR_Specular,
      rhino.TextureType.PBR_SpecularTint,
      rhino.TextureType.PBR_Roughness,
      rhino.TextureType.PBR_Anisotropic,
      rhino.TextureType.PBR_Anisotropic_Rotation,
      rhino.TextureType.PBR_Sheen,
      rhino.TextureType.PBR_SheenTint,
      rhino.TextureType.PBR_Clearcoat,
      rhino.TextureType.PBR_ClearcoatBump,
      rhino.TextureType.PBR_ClearcoatRoughness,
      rhino.TextureType.PBR_OpacityIor,
      rhino.TextureType.PBR_OpacityRoughness,
      rhino.TextureType.PBR_Emission,
      rhino.TextureType.PBR_AmbientOcclusion,
      rhino.TextureType.PBR_Displacement,
    ]

    for (let i = 0; i < doc.materials().count(); i++) {
      const _material = doc.materials().get(i)
      const _pbrMaterial = _material.physicallyBased()

      let material = extractProperties(_material)

      const textures = []

      for (let j = 0; j < textureTypes.length; j++) {
        const _texture = _material.getTexture(textureTypes[j])
        if (_texture) {
          let textureType = textureTypes[j].constructor.name
          textureType = textureType.substring(12, textureType.length)
          const texture = { type: textureType }

          const image = doc.getEmbeddedFileAsBase64(_texture.fileName)

          if (image) {
            texture.image = 'data:image/png;base64,' + image
          } else {
            console.warn(`THREE.3DMLoader: Image for ${textureType} texture not embedded in file.`)
            texture.image = null
          }

          textures.push(texture)

          _texture.delete()
        }
      }

      material.textures = textures

      if (_pbrMaterial.supported) {
        console.log('pbr true')

        for (let j = 0; j < pbrTextureTypes.length; j++) {
          const _texture = _material.getTexture(textureTypes[j])
          if (_texture) {
            const image = doc.getEmbeddedFileAsBase64(_texture.fileName)
            let textureType = textureTypes[j].constructor.name
            textureType = textureType.substring(12, textureType.length)
            const texture = { type: textureType, image: 'data:image/png;base64,' + image }
            textures.push(texture)

            _texture.delete()
          }
        }

        const pbMaterialProperties = extractProperties(_material.physicallyBased())

        material = Object.assign(pbMaterialProperties, material)
      }

      materials.push(material)

      _material.delete()
      _pbrMaterial.delete()
    }

    // Handle layers

    for (let i = 0; i < doc.layers().count(); i++) {
      const _layer = doc.layers().get(i)
      const layer = extractProperties(_layer)

      layers.push(layer)

      _layer.delete()
    }

    // Handle views

    for (let i = 0; i < doc.views().count(); i++) {
      const _view = doc.views().get(i)
      const view = extractProperties(_view)

      views.push(view)

      _view.delete()
    }

    // Handle named views

    for (let i = 0; i < doc.namedViews().count(); i++) {
      const _namedView = doc.namedViews().get(i)
      const namedView = extractProperties(_namedView)

      namedViews.push(namedView)

      _namedView.delete()
    }

    // Handle groups

    for (let i = 0; i < doc.groups().count(); i++) {
      const _group = doc.groups().get(i)
      const group = extractProperties(_group)

      groups.push(group)

      _group.delete()
    }

    // Handle settings

    const settings = extractProperties(doc.settings())

    //TODO: Handle other document stuff like dimstyles, instance definitions, bitmaps etc.

    // Handle dimstyles
    // console.log( `Dimstyle Count: ${doc.dimstyles().count()}` );

    // Handle bitmaps
    // console.log( `Bitmap Count: ${doc.bitmaps().count()}` );

    // Handle strings -- this seems to be broken at the moment in rhino3dm
    // console.log( `Document Strings Count: ${doc.strings().count()}` );

    /*
		for( var i = 0; i < doc.strings().count(); i++ ){

			var _string= doc.strings().get( i );

			console.log(_string);
			var string = extractProperties( _group );

			strings.push( string );

			_string.delete();

		}
		*/

    doc.delete()

    return { objects, materials, layers, views, namedViews, groups, settings }
  }

  function extractObjectData(object, doc) {
    const _geometry = object.geometry()
    const _attributes = object.attributes()
    let objectType = _geometry.objectType
    let geometry, attributes, position, data, mesh

    // skip instance definition objects
    //if( _attributes.isInstanceDefinitionObject ) { continue; }

    // TODO: handle other geometry types
    switch (objectType) {
      case rhino.ObjectType.Curve:
        const pts = curveToPoints(_geometry, 100)

        position = {}
        attributes = {}
        data = {}

        position.itemSize = 3
        position.type = 'Float32Array'
        position.array = []

        for (let j = 0; j < pts.length; j++) {
          position.array.push(pts[j][0])
          position.array.push(pts[j][1])
          position.array.push(pts[j][2])
        }

        attributes.position = position
        data.attributes = attributes

        geometry = { data }

        break

      case rhino.ObjectType.Point:
        const pt = _geometry.location

        position = {}
        const color = {}
        attributes = {}
        data = {}

        position.itemSize = 3
        position.type = 'Float32Array'
        position.array = [pt[0], pt[1], pt[2]]

        const _color = _attributes.drawColor(doc)

        color.itemSize = 3
        color.type = 'Float32Array'
        color.array = [_color.r / 255.0, _color.g / 255.0, _color.b / 255.0]

        attributes.position = position
        attributes.color = color
        data.attributes = attributes

        geometry = { data }

        break

      case rhino.ObjectType.PointSet:
      case rhino.ObjectType.Mesh:
        geometry = _geometry.toThreejsJSON()

        break

      case rhino.ObjectType.Brep:
        const faces = _geometry.faces()
        mesh = new rhino.Mesh()

        for (let faceIndex = 0; faceIndex < faces.count; faceIndex++) {
          const face = faces.get(faceIndex)
          const _mesh = face.getMesh(rhino.MeshType.Any)

          if (_mesh) {
            mesh.append(_mesh)
            _mesh.delete()
          }

          face.delete()
        }

        if (mesh.faces().count > 0) {
          mesh.compact()
          geometry = mesh.toThreejsJSON()
          faces.delete()
        }

        mesh.delete()

        break

      case rhino.ObjectType.Extrusion:
        mesh = _geometry.getMesh(rhino.MeshType.Any)

        if (mesh) {
          geometry = mesh.toThreejsJSON()
          mesh.delete()
        }

        break

      case rhino.ObjectType.TextDot:
        geometry = extractProperties(_geometry)

        break

      case rhino.ObjectType.Light:
        geometry = extractProperties(_geometry)

        break

      case rhino.ObjectType.InstanceReference:
        geometry = extractProperties(_geometry)
        geometry.xform = extractProperties(_geometry.xform)
        geometry.xform.array = _geometry.xform.toFloatArray(true)

        break

      case rhino.ObjectType.SubD:
        // TODO: precalculate resulting vertices and faces and warn on excessive results
        _geometry.subdivide(3)
        mesh = rhino.Mesh.createFromSubDControlNet(_geometry)
        if (mesh) {
          geometry = mesh.toThreejsJSON()
          mesh.delete()
        }

        break

      /*
				case rhino.ObjectType.Annotation:
				case rhino.ObjectType.Hatch:
				case rhino.ObjectType.ClipPlane:
				*/

      default:
        console.warn(`THREE.3DMLoader: TODO: Implement ${objectType.constructor.name}`)
        break
    }

    if (geometry) {
      attributes = extractProperties(_attributes)
      attributes.geometry = extractProperties(_geometry)

      if (_attributes.groupCount > 0) {
        attributes.groupIds = _attributes.getGroupList()
      }

      if (_attributes.userStringCount > 0) {
        attributes.userStrings = _attributes.getUserStrings()
      }

      if (_geometry.userStringCount > 0) {
        attributes.geometry.userStrings = _geometry.getUserStrings()
      }

      attributes.drawColor = _attributes.drawColor(doc)

      objectType = objectType.constructor.name
      objectType = objectType.substring(11, objectType.length)

      return { geometry, attributes, objectType }
    } else {
      console.warn(`THREE.3DMLoader: ${objectType.constructor.name} has no associated mesh geometry.`)
    }
  }

  function extractProperties(object) {
    const result = {}

    for (const property in object) {
      const value = object[property]

      if (typeof value !== 'function') {
        if (typeof value === 'object' && value !== null && value.hasOwnProperty('constructor')) {
          result[property] = { name: value.constructor.name, value: value.value }
        } else {
          result[property] = value
        }
      } else {
        // these are functions that could be called to extract more data.
        //console.log( `${property}: ${object[ property ].constructor.name}` );
      }
    }

    return result
  }

  function curveToPoints(curve, pointLimit) {
    let pointCount = pointLimit
    let rc = []
    const ts = []

    if (curve instanceof rhino.LineCurve) {
      return [curve.pointAtStart, curve.pointAtEnd]
    }

    if (curve instanceof rhino.PolylineCurve) {
      pointCount = curve.pointCount
      for (let i = 0; i < pointCount; i++) {
        rc.push(curve.point(i))
      }

      return rc
    }

    if (curve instanceof rhino.PolyCurve) {
      const segmentCount = curve.segmentCount

      for (let i = 0; i < segmentCount; i++) {
        const segment = curve.segmentCurve(i)
        const segmentArray = curveToPoints(segment, pointCount)
        rc = rc.concat(segmentArray)
        segment.delete()
      }

      return rc
    }

    if (curve instanceof rhino.ArcCurve) {
      pointCount = Math.floor(curve.angleDegrees / 5)
      pointCount = pointCount < 2 ? 2 : pointCount
      // alternative to this hardcoded version: https://stackoverflow.com/a/18499923/2179399
    }

    if (curve instanceof rhino.NurbsCurve && curve.degree === 1) {
      const pLine = curve.tryGetPolyline()

      for (let i = 0; i < pLine.count; i++) {
        rc.push(pLine.get(i))
      }

      pLine.delete()

      return rc
    }

    const domain = curve.domain
    const divisions = pointCount - 1.0

    for (let j = 0; j < pointCount; j++) {
      const t = domain[0] + (j / divisions) * (domain[1] - domain[0])

      if (t === domain[0] || t === domain[1]) {
        ts.push(t)
        continue
      }

      const tan = curve.tangentAt(t)
      const prevTan = curve.tangentAt(ts.slice(-1)[0])

      // Duplicated from THREE.Vector3
      // How to pass imports to worker?

      const tS = tan[0] * tan[0] + tan[1] * tan[1] + tan[2] * tan[2]
      const ptS = prevTan[0] * prevTan[0] + prevTan[1] * prevTan[1] + prevTan[2] * prevTan[2]

      const denominator = Math.sqrt(tS * ptS)

      let angle

      if (denominator === 0) {
        angle = Math.PI / 2
      } else {
        const theta = (tan.x * prevTan.x + tan.y * prevTan.y + tan.z * prevTan.z) / denominator
        angle = Math.acos(Math.max(-1, Math.min(1, theta)))
      }

      if (angle < 0.1) continue

      ts.push(t)
    }

    rc = ts.map((t) => curve.pointAt(t))
    return rc
  }
}

export { Rhino3dmLoader }
